package estrategia;

import java.io.IOException;
import java.util.ArrayList;

import javax.vecmath.Point3d;

import monitor.ManagerObjetos;

import comunicacion.Conector;
import comunicacion.ParametrosEntorno;

public class Deliberador {
	
	private Conector entorno;
	private Utiles utiles;
	private ManagerObjetos managerObjetos;
	
	public Deliberador(Conector entorno, ManagerObjetos managerObjetos) {
		this.entorno = entorno;
		this.managerObjetos = managerObjetos;
		this.utiles = new Utiles(entorno);
	}
	
	
	/**
	 * Esta funcion calcula un punto que es "optimo" para tratar de conquistar la 
	 * mayor cantidad de fuertes posible
	 */
	public Point3d calcularPuntoOptimo(ArrayList<Integer> fuertes) throws IOException {		
		int cantFuertes = fuertes.size();
		int cantCombinaciones = (int)Math.pow(2, cantFuertes);
		
		int cantFuertesPropiosMaxima = -1;
		Point3d mejorPuntoMedio = null;
		int cantFuertesCombinacionMaxima = -1; 
		
		// Utilizamos la representacion binaria, que nos da tocas las combinaciones
		// posibles entre fuertes que "se consideran" o "se dejan de lado"
		// Por ejemplo si mascara=1001 solo se tienen en cuenta el primer y ultimo fuerte de la lista
		for (int mascara=1; mascara<cantCombinaciones; mascara++) {
			// Obtenemos una combinacion dada de entre todas las posible y la evaluamos
			ArrayList<Integer> combinacionActual = filtrarSegunMascara(fuertes, mascara);
			
			// Calculamos el punto resultante
			Point3d puntoActual = promediarPosiciones(combinacionActual);
			// Averiguamos cuantos fuertes estariamos conquistando si vamos a ese punto
			int cantFuertesPropiosTmp = getFuertesConquistables(puntoActual).size();
			int cantFuertesCombinacionActual = combinacionActual.size();	
			// En caso de que encontremos una configuracion mejor que las anteriores,
			// elegimos ese punto medio como el optimo temporal
			if ((cantFuertesPropiosMaxima == cantFuertesPropiosTmp && cantFuertesCombinacionMaxima < cantFuertesCombinacionActual)
					|| (cantFuertesPropiosMaxima < cantFuertesPropiosTmp)) {
				cantFuertesPropiosMaxima = cantFuertesPropiosTmp;
				mejorPuntoMedio = puntoActual;
				// En el caso de empate en cantidad, me interesa considerar la mayor cantidad 
				// posible de fuertes en la combinacion
				cantFuertesCombinacionMaxima = cantFuertesCombinacionActual;
			}
		}
		
		return mejorPuntoMedio;
	}
	
	/**
	 * A partir de la representacion binaria de un numero en complemento a 2 y una coleccion,
	 * filtra la coleccion dejando solamente aquellos valores que se corresponden con bits 1
	 */
	private ArrayList<Integer> filtrarSegunMascara(ArrayList<Integer> coleccion, int mascara) {
		int cantElementos = coleccion.size();
		ArrayList<Integer> validos = new ArrayList<Integer>();
				
		for (int i=0; i<cantElementos; i++) {
			// Obtenemos en shifteado[0] el bit que indica si un elemento
			// debe pertenecer o no a la coleccion filtrada que se devuelve
			int shifteado = Integer.rotateRight(mascara, cantElementos-1-i);
			if ((shifteado & 0x1) == 0x1) {
				// Es valido
				validos.add(new Integer(coleccion.get(i)));				
			} 
		}
		
		return validos;
	}
	
	/**
	 * Devuelve el punto promedio entre las posiciones de las entidades cuyos
	 * indices se indican en la coleccion pasada como parametro
	 */
	private Point3d promediarPosiciones(ArrayList<Integer> coleccion) throws IOException {
		Point3d puntoMedio = new Point3d(0, 0, 0);
		for (Integer idObjeto : coleccion) {
			// TODO aca quizas deberia ser la prediccion no la pos fija, PROBAR Y VER
			Point3d punto = entorno.getCoordenadas(idObjeto);
			puntoMedio.x += punto.x;
			puntoMedio.y += punto.y;
			
		}
		
		double cantElementos = (double) coleccion.size();
		puntoMedio.x = puntoMedio.x / cantElementos;
		puntoMedio.y = puntoMedio.y / cantElementos;
		
		return puntoMedio;
	}
	
	/**
	 * Devuelve la cantidad de fuertes que mi BANDO estaria conquistando si el mundo
	 * se considera estatico en ese momento y la posicion de miRobot pasa a ser posHipotetica
	 */
	public ArrayList<Integer> getFuertesConquistables(Point3d posHipotetica) throws IOException {
		ArrayList<Integer> fuertes = managerObjetos.getFuertes();
		ArrayList<Integer> ejercitos = managerObjetos.getEjercitosActivos();
		
		ArrayList<Integer> fuertesConquistados = new ArrayList<Integer>();
//		System.out.println("EventualPuntoMedio = (" + posHipotetica.x + ", " + posHipotetica.y + ", " + posHipotetica.z + ")");
		
		for (Integer fuerte : fuertes) {
//			System.out.println("Fuerte " + fuerte);
			Point3d posFuerte = entorno.getCoordenadas(fuerte);
			// Por defecto supongo que lo tengo conquistado, y ahi veo si alguien "esta mas cerca"
			double distanciaMinima = utiles.distanciaEuclidea(posHipotetica, posFuerte);
			int ejercitoMasCercano = ParametrosGlobales.MI_ROBOT;
//			System.out.println("Distancia desde ejercito mio = " + distanciaMinima);
			for (Integer ejercito : ejercitos) {
				Point3d posEjercito = entorno.getCoordenadas(ejercito);
				double distancia = utiles.distanciaEuclidea(posEjercito, posFuerte);
//				System.out.println("Distancia desde ejercito " + ejercito + " = " + distancia);
				
				if (distancia < distanciaMinima) {
					distanciaMinima = distancia;
					ejercitoMasCercano = ejercito;
				}
			}
			
//			System.out.println("Ejercito mas cercano al fuerte " + fuerte + " -> " + ejercitoMasCercano);
			
			// Ahora hay que ver el color del ejercito mas cercano al fuerte
			// Eventualmente puedo llegar a ser yo mismo
			if (managerObjetos.esAliado(ejercitoMasCercano)) {
				fuertesConquistados.add(fuerte);
			}
		}
		
//		System.out.println("Fuertes Propios :: " + fuertesPropios);
		
		return fuertesConquistados;
	}
	
	/**
	 * Retorna el identificador del ejercito enemigo que tiene mayor cantidad de fuertes conquistados
	 * En caso de empate devuelve el que se encuentre mas cercano a miRobot
	 */
	public int obtenerMejorEnemigo() throws IOException {
		ArrayList<Integer> fuertes = managerObjetos.getFuertes();
		ArrayList<Integer> ejercitos = managerObjetos.getEjercitosActivos();
		// Agrego a mi robot, para que compita, pero como no es enemigo no pasa nada
		ejercitos.add(new Integer(ParametrosGlobales.MI_ROBOT));
		
		int[] fuertesPorEnemigo = new int[ParametrosGlobales.CANTIDAD_OBJETOS];
		for (int i=0; i<ParametrosGlobales.CANTIDAD_OBJETOS; i++) {
			fuertesPorEnemigo[i] = 0;
		}
		
		Point3d miPos = entorno.getCoordenadas();
		int mejorEnemigo = -1;
		
		for (Integer fuerte : fuertes) {
//			System.out.println("Fuerte " + fuerte);
			Point3d posFuerte = entorno.getCoordenadas(fuerte);
			
			double distanciaMinima = Double.MAX_VALUE;
			int ejercitoMasCercano = -1;

			for (Integer ejercito : ejercitos) {
				Point3d posEjercito = entorno.getCoordenadas(ejercito);
				double distancia = utiles.distanciaEuclidea(posEjercito, posFuerte);
//				System.out.println("Distancia desde ejercito " + ejercito + " = " + distancia);
				
				if (distancia < distanciaMinima) {
					distanciaMinima = distancia;
					ejercitoMasCercano = ejercito;
				}
			}
			
//			System.out.println("Ejercito mas cercano al fuerte " + fuerte + " -> " + ejercitoMasCercano);
		
			if (managerObjetos.esEnemigo(ejercitoMasCercano)) {
				fuertesPorEnemigo[ejercitoMasCercano]++;
				System.out.println("El fuerte " + fuerte + " es del ejercito " + ejercitoMasCercano);
				if (mejorEnemigo < 0 ) {
					mejorEnemigo = ejercitoMasCercano;
				} else if (ejercitoMasCercano != mejorEnemigo) {
					// Revisamos si el enemigo que conquista el fuerte actual pasa a ser el mejor
					double distanciaANuevo = utiles.distanciaEuclidea(miPos, entorno.getCoordenadas(ejercitoMasCercano));
					double distanciaAMejor = utiles.distanciaEuclidea(miPos, entorno.getCoordenadas(mejorEnemigo));
					if ((fuertesPorEnemigo[mejorEnemigo] == fuertesPorEnemigo[ejercitoMasCercano] && distanciaANuevo < distanciaAMejor)
							|| (fuertesPorEnemigo[mejorEnemigo] < fuertesPorEnemigo[ejercitoMasCercano])) {
						mejorEnemigo = ejercitoMasCercano;
					}
				}
			}
		}
		
		for (int ej : ejercitos) {
			System.out.println("Fuertes de " + ej + " :: " + fuertesPorEnemigo[ej]);
		}
		System.out.println("Mejor enemigo -> " + mejorEnemigo + " - " + fuertesPorEnemigo[mejorEnemigo] + " fuertes");
		
		return mejorEnemigo;
	}
	
	/**
	 * Calcula la suma de distancias existentes entre una posicion particular
	 * y cada uno de los fuertes cuyos identificadores de indican en fuertes
	 */
	private double sumatoriaDistanciasAFuertes(Point3d posicion, ArrayList<Integer> fuertes) throws IOException {
		double distanciaTotal = 0;
		for (Integer fuerte : fuertes) {
			Point3d posicionFuerte = entorno.getCoordenadas(fuerte);
			double distancia = utiles.distanciaEuclidea(posicion, posicionFuerte);
			
			distanciaTotal += distancia;
		}
		
		return distanciaTotal;
	}
	
	/**
	 * Devuelve true si la cantidad de fuertes conquistados por el bando de miRobot
	 * es mayor que la cantidad conquistada por el bando contrario
	 */
	public boolean voyGanando() throws IOException {
		Point3d miPos = entorno.getCoordenadas();
		
		int cantFuertes = managerObjetos.getFuertes().size();
		int cantFuertesConquistados = getFuertesConquistables(miPos).size();
		
		// TODO aca hay que ver el tema del empate y eso
		if (cantFuertes - cantFuertesConquistados < cantFuertesConquistados) {
			return true;
		}
		return false;
	}
	
	/**
	 * Determina si hay algun enemigo con posibilidades de sacarnos del juego
	 * En particular chequea si hay alguno en un radio de accion que puede resultar peligroso,
	 * y devuelve su identificador. En caso contrario devuelve -1 
	 */
	public int enemigoAmenzante(Point3d posicion) throws IOException {
		ArrayList<Integer> enemigos = managerObjetos.getEnemigosActivos();
		//Point3d miPos = entorno.getCoordenadas();
		double distanciaMinima = Double.MAX_VALUE;
		int enemigoMasCercano = -1;
		
		for (int i=0; i<enemigos.size(); i++) {
			Point3d posEnemigo = entorno.getCoordenadas(enemigos.get(i));
			
			double distancia = utiles.distanciaEuclidea(posicion, posEnemigo);
			if (distancia < distanciaMinima) {
				distanciaMinima = distancia;
				enemigoMasCercano = enemigos.get(i);
			}			
			
		}
		
		if (distanciaMinima < ParametrosEntorno.DISTANCIA_RANGO_ATAQUE) {
			// Probablemente me estan queriendo atacar
			return enemigoMasCercano;
		}
		
		return -1;
	}
	
	/**
	 * Determina si hay algun objeto muy cerca del punto pasado como parametro.
	 * Considera tanto a fuertes, como a aliados o enemigos 
	 * En particular chequea si hay alguno en un radio de accion bastante pequeño
	 * y devuelve su identificador. En caso contrario devuelve -1 
	 * @throws IOException 
	 */
	public int objetoCercano(Point3d posicion) throws IOException {
		// Inicialmente pedimos todos los objetos, despues obviamos los enemigos
		ArrayList<Integer> objetos = managerObjetos.getObjetosActivos();
		
		//Point3d miPos = entorno.getCoordenadas();
		double distanciaMinima = Double.MAX_VALUE;
		int objetoMasCercano = -1;
		
		for (int i=0; i<objetos.size(); i++) {
			Point3d posEnemigo = entorno.getCoordenadas(objetos.get(i));
			
			double distancia = utiles.distanciaEuclidea(posicion, posEnemigo);
			if (distancia < distanciaMinima) {
				distanciaMinima = distancia;
				objetoMasCercano = objetos.get(i);
			}			
			
		}
		
		if (distanciaMinima < ParametrosEntorno.DISTANCIA_PELIGRO_COLISION) {
			return objetoMasCercano;
		}
		
		return -1;
	}
	
	/**
	 * Busca un punto en el entorno del punto actual, tal que mantenga al menos la misma cantidad de 
	 * fuertes conquistados pero que de ser posible disminuya la distancia sumada total a todos los
	 * fuertes del bando propio 
	 */
	public Point3d mejorarPuntoOptimo() throws IOException {
		int CANT_EVALUACIONES = 16;
		double RADIO_EVALUACION = 0.15;
		double DISTANCIA_MINIMA_MEJORA = 0.1;
		
		Point3d posActual = entorno.getCoordenadas();
		ArrayList<Integer> fuertesConquistados = getFuertesConquistables(posActual);
		int cantFuertesConquistables = fuertesConquistados.size();
		double distanciaAcumuladaMinima = sumatoriaDistanciasAFuertes(posActual, fuertesConquistados);
		// Si no disminuye la medicion en mas de DISTANCIA_MINIMA_MEJORA, entonces no vale la pena
		distanciaAcumuladaMinima -= DISTANCIA_MINIMA_MEJORA;
		Point3d mejorPunto = null;
		
		for (int i=0; i<CANT_EVALUACIONES; i++) {
			double angulo = -Math.PI + i*2.0*Math.PI/(double)CANT_EVALUACIONES;
			Point3d posNueva = new Point3d();
			posNueva.x = posActual.x + RADIO_EVALUACION*Math.cos(angulo);
			posNueva.y = posActual.y + RADIO_EVALUACION*Math.sin(angulo);
			
			// Chequeamos que el punto no cae en algun fuerte o ejercito
			if (objetoCercano(posNueva) < 0) {			
				// Solamente lo tenemos en cuenta si esta "libre", de lo contrario
				// no vale la pena buscar un punto porque en definitiva se va a elegir
				// tomar como referencia el objeto que esta cerca y no el propio punto
				double distanciaAcumuladaTmp = sumatoriaDistanciasAFuertes(posNueva, fuertesConquistados);
				ArrayList<Integer> fuertesMantenibles = getFuertesConquistables(posNueva);
				if ((fuertesMantenibles.size() == cantFuertesConquistables && distanciaAcumuladaTmp < distanciaAcumuladaMinima) 
						|| (fuertesMantenibles.size() > cantFuertesConquistables)) {
					cantFuertesConquistables = fuertesMantenibles.size();
					distanciaAcumuladaMinima = distanciaAcumuladaTmp;
					
					// Para que vaya al punto en realdiad tengo que mandarlo un poco mas lejos
					// En particular, distancia aceptable mas lejos. Esto es solo un truco medio sucio
					mejorPunto = new Point3d();
					double distancia = RADIO_EVALUACION + ParametrosPotenciales.DISTANCIA_PUNTO;
					mejorPunto.x = posActual.x + distancia*Math.cos(angulo);
					mejorPunto.y = posActual.y + distancia*Math.sin(angulo);
				}
			}
		}
		
		return mejorPunto;
	}
}
